﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.CompilerServices;
using AssetStudio;
using AssetStudio.Extended.CompositeModels;
using AssetStudio.Extended.CompositeModels.Utilities;
using Imas.Data.Serialized.Sway;
using JetBrains.Annotations;
using OpenMLTD.MillionDance.Entities.Internal;
using OpenMLTD.MillionDance.Entities.Pmx;
using OpenMLTD.MillionDance.Entities.Pmx.Extensions;
using OpenMLTD.MillionDance.Extensions;
using OpenMLTD.MillionDance.Utilities;
using OpenTK;
using BlendShapeData = AssetStudio.Extended.CompositeModels.BlendShapeData;
using Color = System.Drawing.Color;
using Quaternion = OpenTK.Quaternion;
using Vector2 = OpenTK.Vector2;
using Vector3 = OpenTK.Vector3;
using Vector4 = OpenTK.Vector4;

namespace OpenMLTD.MillionDance.Core {
    partial class PmxCreator {

        [NotNull]
        public PmxModel CreateModel([NotNull] CompositeAvatar combinedAvatar, [NotNull] CompositeMesh combinedMesh, int bodyMeshVertexCount,
            [NotNull] SwayController bodySway, [NotNull] SwayController headSway,
            [NotNull] ConversionDetails details, out BakedMaterial[] materialList) {
            var model = new PmxModel();

            model.Name = "ミリシタ モデル00";
            model.NameEnglish = "MODEL_00";
            model.Comment = "製作：MillionDance" + Environment.NewLine + "©BANDAI NAMCO Entertainment Inc.";
            model.CommentEnglish = "Generated by MillionDance" + Environment.NewLine + "©BANDAI NAMCO Entertainment Inc.";
            model.UvaCount = _conversionConfig.GetAdditionalUvCount();

            var vertices = AddVertices(combinedAvatar, combinedMesh, bodyMeshVertexCount);
            model.Vertices = vertices;

            var indices = AddIndices(combinedMesh);
            model.FaceTriangles = indices;

            var bones = AddBones(combinedAvatar, combinedMesh, vertices);
            model.Bones = bones;

            bones.AssertAllUnique();

            if (_conversionConfig.FixTdaBindingPose) {
                if (_conversionConfig.SkeletonFormat == SkeletonFormat.Mmd) {
                    if (_conversionConfig.TranslateBoneNamesToMmd) {
                        FixTdaBonesAndVertices(bones, vertices);
                    }
                } else if (_conversionConfig.SkeletonFormat == SkeletonFormat.Mltd) {
                } else {
                    throw new NotSupportedException("You must choose a motion source to determine skeleton format.");
                }
            }

            var materials = AddMaterials(combinedMesh, details, model, out materialList);
            model.Materials = materials;

            var emotionMorphs = AddEmotionMorphs(combinedMesh);
            model.Morphs = emotionMorphs;

            // PMX Editor requires at least one node (root), or it will crash because these code:
            /**
             * this.PXRootNode = new PXNode(base.RootNode);
             * this.PXExpressionNode = new PXNode(base.ExpNode);
             * this.PXNode.Clear();
             * this.PXNode.Capacity = base.NodeList.Count - 1; // notice this line
             */
            var nodes = AddBoneNodesAsGroups(bones, emotionMorphs);
            model.Nodes = nodes;

            if (_conversionConfig.ImportPhysics) {
                var physics = new Physics(this);
                (model.RigidBodies, model.Joints) = physics.ImportPhysics(bones, bodySway, headSway);
            }

            return model;
        }

        [NotNull, ItemNotNull]
        private PmxVertex[] AddVertices([NotNull] CompositeAvatar combinedAvatar, [NotNull] CompositeMesh combinedMesh, int bodyMeshVertexCount) {
            var vertexCount = combinedMesh.VertexCount;
            var vertices = new PmxVertex[vertexCount];
            // In case that vertex count is more than skin count (ill-formed MLTD models: ch_ex005_022ser)
            var skinCount = combinedMesh.Skin.Length;

            var shouldScaleToPmxSize = _conversionConfig.ScaleToPmxSize;
            var scaleUnityToPmx = _scalingConfig.ScaleUnityToPmx;

            for (var i = 0; i < vertexCount; ++i) {
                var vertex = new PmxVertex();

                var position = combinedMesh.Vertices[i];
                var normal = combinedMesh.Normals[i];
                var uv1 = combinedMesh.UV1[i];
                var uv2 = combinedMesh.UV2[i];

                vertex.Position = position.ToOpenTK().FixUnityToMmd();

                if (shouldScaleToPmxSize) {
                    vertex.Position = vertex.Position * scaleUnityToPmx;
                }

                vertex.Normal = normal.ToOpenTK().FixUnityToMmd();

                Vector2 fixedUv1;
                Vector4 fixedUv2;

                // Body, then head.
                // TODO: For heads, inverting/flipping is different among models?
                // e.g. ss001_015siz can be processed via the method below; gs001_201xxx's head UVs are not inverted but some are flipped.
                if (i < bodyMeshVertexCount) {
                    // Flip V!
                    fixedUv1 = new Vector2(uv1.X, 1 - uv1.Y);
                } else {
                    fixedUv1 = uv1.ToOpenTK();
                }

                if (uv2.HasValue) {
                    var v = uv2.Value;

                    // Set highlight texture UV
                    if (i < bodyMeshVertexCount) {
                        // Flip V!
                        fixedUv2 = new Vector4(v.X, 1 - v.Y, 0, 0);
                    } else {
                        fixedUv2 = new Vector4(v.X, v.Y, 0, 0);
                    }
                } else {
                    fixedUv2 = Vector4.Zero;
                }

                vertex.UV = fixedUv1;
                vertex.Uva[0] = fixedUv2;

                vertex.EdgeScale = 1.0f;

                var skin = i < skinCount ? combinedMesh.Skin[i] : null;
                var affectiveInfluenceCount = skin?.Count(influence => influence != null) ?? 0;

                switch (affectiveInfluenceCount) {
                    case 0:
                        // This vertex is static. It is not attached to any bone.
                        break;
                    case 1:
                        vertex.Deformation = Deformation.Bdef1;
                        break;
                    case 2:
                        vertex.Deformation = Deformation.Bdef2;
                        break;
                    case 3:
                        throw new NotSupportedException($"Not supported: vertex #{i.ToString()} has 3 influences.");
                    case 4:
                        vertex.Deformation = Deformation.Bdef4;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(affectiveInfluenceCount), "Unsupported number of bones.");
                }

                Debug.Assert(skin != null || affectiveInfluenceCount == 0, nameof(skin) + " != null || " + nameof(affectiveInfluenceCount) + " == 0");

                for (var j = 0; j < affectiveInfluenceCount; ++j) {
                    Debug.Assert(skin != null);

                    var boneId = combinedMesh.BoneNameHashes[skin[j].BoneIndex];
                    var realBoneIndex = combinedAvatar.AvatarSkeleton.NodeIDs.FindIndex(boneId);

                    if (realBoneIndex < 0) {
                        throw new ArgumentOutOfRangeException(nameof(realBoneIndex));
                    }

                    vertex.BoneWeights[j].BoneIndex = realBoneIndex;
                    vertex.BoneWeights[j].Weight = skin[j].Weight;
                }

                vertices[i] = vertex;
            }

            return vertices;
        }

        [NotNull]
        private static int[] AddIndices([NotNull] CompositeMesh combinedMesh) {
            var indices = new int[combinedMesh.Indices.Length];

            for (var i = 0; i < indices.Length; ++i) {
                indices[i] = unchecked((int)combinedMesh.Indices[i]);
            }

            return indices;
        }

        [NotNull, ItemNotNull]
        private List<PmxBone> GetBonesList([NotNull] CompositeAvatar combinedAvatar) {
            var boneCount = combinedAvatar.AvatarSkeleton.NodeIDs.Length;
            var bones = new List<PmxBone>(boneCount);

            var hierarchy = _boneLookup.BuildBoneHierarchy(combinedAvatar);

            var considerIdolHeight = _conversionConfig.ApplyPmxCharacterHeight;
            var factor = _scalingConfig.CharacterHeightScalingFactor;

            for (var i = 0; i < boneCount; ++i) {
                var bone = new PmxBone();
                var transform = combinedAvatar.AvatarSkeletonPose.Transforms[i];
                var boneNode = hierarchy[i];

                var pmxBoneName = _boneLookup.GetPmxBoneName(boneNode.Path);

                bone.Name = pmxBoneName;
                bone.NameEnglish = BoneLookup.TranslateBoneName(pmxBoneName, pmxBoneName);

                // PMX's bone positions are in world coordinate system.
                // Unity's are in local coords.
                bone.InitialPosition = boneNode.InitialPositionWorld;

                if (considerIdolHeight) {
                    bone.InitialPosition = bone.InitialPosition * factor;
                }

                bone.CurrentPosition = bone.InitialPosition;

                bone.ParentIndex = boneNode.Parent?.Index ?? -1;
                bone.BoneIndex = i;

                var singleDirectChild = GetDirectSingleChildOf(boneNode);

                if (singleDirectChild != null) {
                    bone.SetFlag(BoneFlags.ToBone);
                    bone.To_Bone = singleDirectChild.Index;
                } else {
                    // TODO: Fix this; it should point to a world position.
                    bone.To_Offset = transform.LocalPosition.ToOpenTK().FixUnityToMmd();
                }

                // No use. This is just a flag to specify more details to rotation/translation limitation.
                //bone.SetFlag(BoneFlags.LocalFrame);
                bone.InitialRotation = transform.LocalRotation.ToOpenTK().FixUnityToOpenTK();
                bone.CurrentRotation = bone.InitialRotation;

                //bone.Level = boneNode.Level;
                bone.Level = 0;

                if (BoneLookup.IsBoneMovable(boneNode.Path)) {
                    bone.SetFlag(BoneFlags.Rotation | BoneFlags.Translation);
                } else {
                    bone.SetFlag(BoneFlags.Rotation);
                }

                if (_conversionConfig.HideUnityGeneratedBones) {
                    if (BoneLookup.IsNameGenerated(boneNode.Path)) {
                        bone.ClearFlag(BoneFlags.Visible);
                    }
                }

                bones.Add(bone);
            }

            return bones;
        }

        [NotNull, ItemNotNull]
        private List<PmxBone> GetBonesList([NotNull] TransformHierarchies transformHierarchies) {
            var orderedTransformHierarchy = transformHierarchies.GetOrderedObjectArray();
            var boneCount = orderedTransformHierarchy.Length;
            var bones = new List<PmxBone>(boneCount);

            var hierarchy = _boneLookup.BuildBoneHierarchy(transformHierarchies);

            var considerIdolHeight = _conversionConfig.ApplyPmxCharacterHeight;
            var factor = _scalingConfig.CharacterHeightScalingFactor;

            for (var i = 0; i < boneCount; ++i) {
                var bone = new PmxBone();
                var transform = orderedTransformHierarchy[i].Transform;
                var boneNode = hierarchy[i];

                var pmxBoneName = _boneLookup.GetPmxBoneName(boneNode.Path);

                bone.Name = pmxBoneName;
                bone.NameEnglish = BoneLookup.TranslateBoneName(pmxBoneName, pmxBoneName);

                // PMX's bone positions are in world coordinate system.
                // Unity's are in local coords.
                bone.InitialPosition = boneNode.InitialPositionWorld;

                if (considerIdolHeight) {
                    bone.InitialPosition = bone.InitialPosition * factor;
                }

                bone.CurrentPosition = bone.InitialPosition;

                bone.ParentIndex = boneNode.Parent?.Index ?? -1;
                bone.BoneIndex = i;

                var singleDirectChild = GetDirectSingleChildOf(boneNode);

                if (singleDirectChild != null) {
                    bone.SetFlag(BoneFlags.ToBone);
                    bone.To_Bone = singleDirectChild.Index;
                } else {
                    // TODO: According to docs it should point to a world position?
                    bone.To_Offset = transform.LocalPosition.ToOpenTK().FixUnityToMmd();
                }

                // No use. This is just a flag to specify more details to rotation/translation limitation.
                //bone.SetFlag(BoneFlags.LocalFrame);
                bone.InitialRotation = transform.LocalRotation.ToOpenTK().FixUnityToOpenTK();
                bone.CurrentRotation = bone.InitialRotation;

                //bone.Level = boneNode.Level;
                bone.Level = 0;

                if (BoneLookup.IsBoneMovable(boneNode.Path)) {
                    bone.SetFlag(BoneFlags.Rotation | BoneFlags.Translation);
                } else {
                    bone.SetFlag(BoneFlags.Rotation);
                }

                if (_conversionConfig.HideUnityGeneratedBones) {
                    if (BoneLookup.IsNameGenerated(boneNode.Path)) {
                        bone.ClearFlag(BoneFlags.Visible);
                    }
                }

                bones.Add(bone);
            }

            return bones;
        }

        [NotNull, ItemNotNull]
        private PmxBone[] AddBones([NotNull] TransformHierarchies transformHierarchies, [NotNull] CompositeMesh combinedMesh, [NotNull, ItemNotNull] PmxVertex[] vertices) {
            var bones = GetBonesList(transformHierarchies);
            FixBones(bones, combinedMesh, vertices);
            return bones.ToArray();
        }

        [NotNull, ItemNotNull]
        private PmxBone[] AddBones([NotNull] CompositeAvatar combinedAvatar, [NotNull] CompositeMesh combinedMesh, [NotNull, ItemNotNull] PmxVertex[] vertices) {
            var bones = GetBonesList(combinedAvatar);
            FixBones(bones, combinedMesh, vertices);
            return bones.ToArray();
        }

        private void FixBones([NotNull, ItemNotNull] List<PmxBone> bones, [NotNull] CompositeMesh combinedMesh, [NotNull, ItemNotNull] PmxVertex[] vertices) {
            if (_conversionConfig.FixMmdCenterBones) {
                // Add master (全ての親) and center (センター), recompute bone hierarchy.
                PmxBone master = new PmxBone(), center = new PmxBone();

                master.Name = "全ての親";
                master.NameEnglish = "master";
                center.Name = "センター";
                center.NameEnglish = "center";

                master.ParentIndex = 0; // "" bone
                center.ParentIndex = 1; // "master" bone

                master.CurrentPosition = master.InitialPosition = Vector3.Zero;
                center.CurrentPosition = center.InitialPosition = Vector3.Zero;

                master.SetFlag(BoneFlags.Translation | BoneFlags.Rotation);
                center.SetFlag(BoneFlags.Translation | BoneFlags.Rotation);

                bones.Insert(1, master);
                bones.Insert(2, center);

                //// Fix "MODEL_00" bone

                //do {
                //    var model00 = bones.Find(b => b.Name == "グルーブ");

                //    if (model00 == null) {
                //        throw new ArgumentException("MODEL_00 mapped bone is not found.");
                //    }

                //    model00.ParentIndex = 2; // "center" bone
                //} while (false);

                const int numBonesAdded = 2;

                // Fix vertices and other bones
                foreach (var vertex in vertices) {
                    foreach (var boneWeight in vertex.BoneWeights) {
                        if (boneWeight.BoneIndex == 0 && boneWeight.Weight <= 0) {
                            continue;
                        }

                        if (boneWeight.BoneIndex >= 1) {
                            boneWeight.BoneIndex += numBonesAdded;
                        }
                    }
                }

                for (var i = numBonesAdded + 1; i < bones.Count; ++i) {
                    var bone = bones[i];

                    bone.ParentIndex += numBonesAdded;

                    if (bone.HasFlag(BoneFlags.ToBone)) {
                        bone.To_Bone += numBonesAdded;
                    }
                }
            }

            if (_conversionConfig.AppendIKBones) {
                // Add IK bones.

                // ReSharper disable once InconsistentNaming
                var leftLegIK = CreateLegIK(bones, "左", "L");
                bones.AddRange(leftLegIK);
                // ReSharper disable once InconsistentNaming
                var rightLegIK = CreateLegIK(bones, "右", "R");
                bones.AddRange(rightLegIK);

                // ReSharper disable once InconsistentNaming
                var leftToeIK = CreateToeIK(bones, "左", "L");
                bones.AddRange(leftToeIK);
                // ReSharper disable once InconsistentNaming
                var rightToeIK = CreateToeIK(bones, "右", "R");
                bones.AddRange(rightToeIK);
            }

            if (_conversionConfig.AppendEyeBones) {
                var (vs1, vc1, vs2, vc2) = FindEyesVerticesRange(combinedMesh);
                PmxBone head;

                do {
                    head = bones.Find(b => b.Name == "頭");

                    if (head == null) {
                        throw new ArgumentException("Missing head bone.");
                    }
                } while (false);

                var eyes = new PmxBone();

                eyes.Name = "両目";
                eyes.NameEnglish = "eyes";

                eyes.Parent = head;
                eyes.ParentIndex = bones.IndexOf(head);

                eyes.CurrentPosition = eyes.InitialPosition = GetEyesBonePosition(vertices, vs1, vc1, vs2, vc2);

                eyes.SetFlag(BoneFlags.Visible | BoneFlags.Rotation | BoneFlags.ToBone);
                eyes.To_Bone = -1;

                bones.Add(eyes);

                PmxBone leftEye = new PmxBone(), rightEye = new PmxBone();

                leftEye.Name = "左目";
                leftEye.NameEnglish = "eye_L";
                rightEye.Name = "右目";
                rightEye.NameEnglish = "eye_R";

                leftEye.Parent = head;
                leftEye.ParentIndex = bones.IndexOf(head);
                rightEye.Parent = head;
                rightEye.ParentIndex = bones.IndexOf(head);

                leftEye.SetFlag(BoneFlags.Visible | BoneFlags.Rotation | BoneFlags.ToBone | BoneFlags.AppendRotation);
                rightEye.SetFlag(BoneFlags.Visible | BoneFlags.Rotation | BoneFlags.ToBone | BoneFlags.AppendRotation);
                leftEye.To_Bone = -1;
                rightEye.To_Bone = -1;
                leftEye.AppendParent = eyes;
                rightEye.AppendParent = eyes;
                leftEye.AppendParentIndex = bones.IndexOf(eyes);
                rightEye.AppendParentIndex = bones.IndexOf(eyes);
                leftEye.AppendRatio = 1;
                rightEye.AppendRatio = 1;

                leftEye.CurrentPosition = leftEye.InitialPosition = GetEyeBonePosition(vertices, vs1, vc1);
                rightEye.CurrentPosition = rightEye.InitialPosition = GetEyeBonePosition(vertices, vs2, vc2);

                bones.Add(leftEye);
                bones.Add(rightEye);

                // Fix vertices
                {
                    var leftEyeIndex = bones.IndexOf(leftEye);
                    var rightEyeIndex = bones.IndexOf(rightEye);

                    for (var i = vs1; i < vs1 + vc1; ++i) {
                        var skin = vertices[i];
                        // Eyes are only affected by "KUBI/ATAMA" bone by default. So we only need to set one element's values.
                        skin.BoneWeights[0].BoneIndex = leftEyeIndex;
                        Debug.Assert(Math.Abs(skin.BoneWeights[0].Weight - 1) < 0.000001f, "Total weight in the skin of left eye should be 1.");
                    }

                    for (var i = vs2; i < vs2 + vc2; ++i) {
                        var skin = vertices[i];
                        // Eyes are only affected by "KUBI/ATAMA" bone by default. So we only need to set one element's values.
                        skin.BoneWeights[0].BoneIndex = rightEyeIndex;
                        Debug.Assert(Math.Abs(skin.BoneWeights[0].Weight - 1) < 0.000001f, "Total weight in the skin of right eye should be 1.");
                    }
                }
            }

            // Finally, set the indices. The values will be used later.
            for (var i = 0; i < bones.Count; i++) {
                bones[i].BoneIndex = i;
            }
        }

        // Change standard T-pose to TDA T-pose
        private static void FixTdaBonesAndVertices([NotNull, ItemNotNull] PmxBone[] bones, [NotNull, ItemNotNull] PmxVertex[] vertices) {
            var defRotRight = Quaternion.FromEulerAngles(0, 0, MathHelper.DegreesToRadians(34.5f));
            var defRotLeft = Quaternion.FromEulerAngles(0, 0, MathHelper.DegreesToRadians(-34.5f));

            // Uniqueness is asserted above
            var leftArm = bones.Find(b => b.Name == "左腕");
            var rightArm = bones.Find(b => b.Name == "右腕");

            Debug.Assert(leftArm != null, nameof(leftArm) + " != null");
            Debug.Assert(rightArm != null, nameof(rightArm) + " != null");

            leftArm.AnimatedRotation = defRotLeft;
            rightArm.AnimatedRotation = defRotRight;

            foreach (var bone in bones) {
                if (bone.ParentIndex >= 0) {
                    bone.Parent = bones[bone.ParentIndex];
                }
            }

            foreach (var bone in bones) {
                bone.SetToVmdPose(true);
            }

            foreach (var vertex in vertices) {
                var m = Matrix4.Zero;

                for (var j = 0; j < 4; ++j) {
                    var boneWeight = vertex.BoneWeights[j];

                    if (!boneWeight.IsValid) {
                        continue;
                    }

                    m = m + bones[boneWeight.BoneIndex].SkinMatrix * boneWeight.Weight;
                }

                vertex.Position = Vector3.TransformPosition(vertex.Position, m);
                vertex.Normal = Vector3.TransformNormal(vertex.Normal, m);
            }

            foreach (var bone in bones) {
                bone.InitialPosition = bone.CurrentPosition;
            }
        }

        [NotNull, ItemNotNull]
        private PmxMaterial[] AddMaterials([NotNull] PrettyMesh combinedMesh, [NotNull] ConversionDetails details, [NotNull] PmxModel model, [NotNull] out BakedMaterial[] bakedMaterials) {
            var subMeshCount = combinedMesh.SubMeshes.Length;
            var materialList = new List<PmxMaterial>();
            var extraMaterialList = new List<(int IndexStart, int IndexCount, MaterialKind MaterialKind, PmxMaterial Material)>();
            var bakedMaterialList = new List<BakedMaterial>();
            var extraBakedMaterialList = new List<BakedMaterial>();
            var textureIndex = 0;
            var indexStart = 0;
            var highlightIndex = 0;

            var bakeHairHighlights = _conversionConfig.AddHairHighlights;
            var bakeEyesHighlights = _conversionConfig.AddEyesHighlights;

            for (var i = 0; i < subMeshCount; ++i) {
                var subMesh = combinedMesh.SubMeshes[i];
                var material = new PmxMaterial();
                var materialName = subMesh.Material.MaterialName;

                material.Name = materialName;
                material.NameEnglish = materialName;
                material.AppliedFaceVertexCount = (int)subMesh.IndexCount;
                material.Ambient = new Vector3(0.5f, 0.5f, 0.5f);
                material.Diffuse = Vector4.One;
                material.Specular = Vector3.Zero;
                material.EdgeColor = new Vector4(0.3f, 0.3f, 0.3f, 0.8f);
                material.EdgeSize = 1.0f;
                material.TextureFileName = GetExportedTextureFileName(details.TexturePrefix, textureIndex);

                string toonStr;
                MaterialFlags materialFlags;

                var materialCategory = CategorizeMaterial(materialName);

                BakedMaterial mainBakedMaterial;
                BakedMaterial subBakedMaterial = null;

                switch (materialCategory) {
                    case MaterialKind.BodySkin: {
                        if (details.ApplyToon) {
                            // Only apply toon on body, not on head. (Shadows on head is pre-baked.)
                            // (What about accessories on head? Currently we also apply toon on them.)
                            // "BNSI seems to call this technique 'lively toon'?" - wikisong
                            toonStr = details.SkinToonNumber.ToString("00");
                        } else {
                            toonStr = null;
                        }

                        materialFlags = MaterialFlags.GroundShadow | MaterialFlags.CullNone | MaterialFlags.Edge | MaterialFlags.ReceiveShadow | MaterialFlags.DrawShadow;
                        mainBakedMaterial = CreateBakedMaterialFromBaseTexture(material.TextureFileName, subMesh.Material);

                        break;
                    }
                    case MaterialKind.FacialSkin: {
                        toonStr = null;
                        materialFlags = MaterialFlags.GroundShadow | MaterialFlags.CullNone | MaterialFlags.Edge | MaterialFlags.ReceiveShadow | MaterialFlags.DrawShadow;
                        mainBakedMaterial = CreateBakedMaterialFromBaseTexture(material.TextureFileName, subMesh.Material);
                        break;
                    }
                    case MaterialKind.Hair: {
                        toonStr = null;
                        materialFlags = MaterialFlags.GroundShadow | MaterialFlags.CullNone | MaterialFlags.Edge | MaterialFlags.ReceiveShadow | MaterialFlags.DrawShadow;

                        mainBakedMaterial = CreateBakedMaterialFromBaseTexture(material.TextureFileName, subMesh.Material);

                        if (bakeHairHighlights) {
                            textureIndex += 1;
                            var subTextureName = GetExportedTextureFileName(details.TexturePrefix, textureIndex);
                            subBakedMaterial = CreateHighlightBakedMaterial(subTextureName, subMesh.Material);
                        }

                        break;
                    }
                    case MaterialKind.Eyes: {
                        toonStr = null;
                        materialFlags = MaterialFlags.CullNone;

                        mainBakedMaterial = CreateBakedMaterialFromBaseTexture(material.TextureFileName, subMesh.Material);

                        if (bakeEyesHighlights) {
                            textureIndex += 1;
                            var subTextureName = GetExportedTextureFileName(details.TexturePrefix, textureIndex);
                            subBakedMaterial = CreateHighlightBakedMaterial(subTextureName, subMesh.Material);
                        }

                        break;
                    }
                    case MaterialKind.Clothes: {
                        if (details.ApplyToon) {
                            toonStr = details.ClothesToonNumber.ToString("00");
                        } else {
                            toonStr = null;
                        }

                        materialFlags = MaterialFlags.GroundShadow | MaterialFlags.CullNone | MaterialFlags.Edge | MaterialFlags.ReceiveShadow | MaterialFlags.DrawShadow;
                        mainBakedMaterial = CreateBakedMaterialFromBaseTexture(material.TextureFileName, subMesh.Material);

                        break;
                    }
                    case MaterialKind.Accessories: {
                        if (details.ApplyToon) {
                            toonStr = details.ClothesToonNumber.ToString("00");
                        } else {
                            toonStr = null;
                        }

                        // Don't draw edge on "cut" materials otherwise it looks really weird
                        materialFlags = MaterialFlags.GroundShadow | MaterialFlags.CullNone | MaterialFlags.ReceiveShadow | MaterialFlags.DrawShadow;
                        mainBakedMaterial = CreateBakedMaterialFromBaseTexture(material.TextureFileName, subMesh.Material);

                        break;
                    }
                    default:
                        throw new ArgumentOutOfRangeException(nameof(materialCategory), materialCategory, null);
                }

                if (toonStr != null) {
                    material.ToonTextureFileName = $"toon{toonStr}.bmp";
                }

                material.Flags = materialFlags;

                materialList.Add(material);
                bakedMaterialList.Add(mainBakedMaterial);

                if (subBakedMaterial != null) {
                    extraBakedMaterialList.Add(subBakedMaterial);

                    var extraMaterial = CreateHighlightCopyFromBaseMaterial(material, subMesh.Material, subBakedMaterial, highlightIndex);
                    extraMaterialList.Add((indexStart, material.AppliedFaceVertexCount, materialCategory, extraMaterial));
                    highlightIndex += 1;
                }

                textureIndex += 1;
                indexStart += material.AppliedFaceVertexCount;
            }

            if (extraBakedMaterialList.Count > 0) {
                bakedMaterialList.AddRange(extraBakedMaterialList);
            }

            bakedMaterials = bakedMaterialList.ToArray();

            if (extraMaterialList.Count > 0) {
                var vertices = new List<PmxVertex>(model.Vertices);
                var faces = new List<int>(model.FaceTriangles);

                for (var i = 0; i < extraMaterialList.Count; i++) {
                    var bakedMaterial = extraBakedMaterialList[i];
                    var t = extraMaterialList[i];

                    switch (t.MaterialKind) {
                        case MaterialKind.Eyes:
                            FinishExtraMaterialForEyes(t.Material, t.IndexStart, t.IndexCount, vertices, faces);
                            break;
                        case MaterialKind.Hair:
                            FinishExtraMaterialForHair(t.Material, t.IndexStart, t.IndexCount, vertices, faces, bakedMaterial);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException(nameof(t.MaterialKind), t.MaterialKind, null);
                    }

                    materialList.Add(t.Material);
                }

                model.Vertices = vertices.ToArray();
                model.FaceTriangles = faces.ToArray();
            }

            return materialList.ToArray();
        }

        // Compute and set index start and count for the new material, then duplicate the vertices and faces
        private static void FinishExtraMaterialForHair([NotNull] PmxMaterial material, int indexStart, int indexCount, [NotNull, ItemNotNull] List<PmxVertex> vertices, [NotNull] List<int> faces, [NotNull] BakedMaterial bakedMaterial) {
            var addedFaces = new HashSet<(PmxVertex, PmxVertex, PmxVertex)>();
            var affectedFaces = new List<PmxVertex>();

            var imageWidth = bakedMaterial.BakedTexture.Width;
            var imageHeight = bakedMaterial.BakedTexture.Height;

            using (var access = new DirectBitmapAccess(bakedMaterial.BakedTexture, ImageLockMode.ReadOnly, true)) {
                for (var y = 0; y < imageHeight; y += 1) {
                    var v = (float)y / (imageHeight - 1);

                    for (var x = 0; x < imageWidth; x += 1) {
                        var u = (float)x / (imageWidth - 1);

                        var color = access.GetPixel(x, y);

                        // We are not interested in transparent pixels
                        if (color.A == 0) {
                            continue;
                        }

                        var uv = new Vector2(u, v);

                        // Which face(s) in current submesh referenced this pixel?
                        for (var i = 0; i < indexCount; i += 3) {
                            var index0 = faces[indexStart + i];
                            var index1 = faces[indexStart + i + 1];
                            var index2 = faces[indexStart + i + 2];
                            var vertex0 = vertices[index0];
                            var vertex1 = vertices[index1];
                            var vertex2 = vertices[index2];

                            var uva0 = vertex0.Uva[0].Xy;
                            var uva1 = vertex1.Uva[0].Xy;
                            var uva2 = vertex2.Uva[0].Xy;

                            if (!GeometryHelper.PointInTriangle(in uv, in uva0, in uva1, in uva2)) {
                                continue;
                            }

                            var face = (vertex0, vertex1, vertex2);

                            if (addedFaces.Contains(face)) {
                                continue;
                            }

                            // TODO: We can still trim the duplicate vertices
                            affectedFaces.Add(vertex0);
                            affectedFaces.Add(vertex1);
                            affectedFaces.Add(vertex2);
                            addedFaces.Add(face);
                        }
                    }
                }
            }

            var indexIndex = vertices.Count;
            foreach (var vertex in affectedFaces) {
                vertices.Add(vertex.Clone());
                faces.Add(indexIndex);
                indexIndex += 1;
            }

            material.AppliedFaceVertexCount = affectedFaces.Count;
        }

        private static void FinishExtraMaterialForEyes([NotNull] PmxMaterial material, int indexStart, int indexCount, [NotNull, ItemNotNull] List<PmxVertex> vertices, [NotNull] List<int> faces) {
            var affectedFaces = new List<PmxVertex>();

            for (var i = 0; i < indexCount; i += 3) {
                var index0 = faces[indexStart + i];
                var index1 = faces[indexStart + i + 1];
                var index2 = faces[indexStart + i + 2];
                var vertex0 = vertices[index0];
                var vertex1 = vertices[index1];
                var vertex2 = vertices[index2];

                // TODO: We can still trim the duplicate vertices
                affectedFaces.Add(vertex0);
                affectedFaces.Add(vertex1);
                affectedFaces.Add(vertex2);
            }

            var indexIndex = vertices.Count;
            foreach (var vertex in affectedFaces) {
                var v = vertex.Clone();

                // Directly use base texture's UV for highlight UV (since texture sizes match)
                v.Uva[0] = new Vector4(v.UV.X, v.UV.Y, 0, 0);

                vertices.Add(v);
                faces.Add(indexIndex);
                indexIndex += 1;
            }

            material.AppliedFaceVertexCount = affectedFaces.Count;
        }

        [NotNull]
        private static PmxMaterial CreateHighlightCopyFromBaseMaterial([NotNull] PmxMaterial material, [NotNull] TexturedMaterial texturedMaterial, [NotNull] BakedMaterial bakedMaterial, int highlightIndex) {
            var result = new PmxMaterial();

            var subTexture = texturedMaterial.SubTexture;
            Debug.Assert(subTexture != null, nameof(subTexture) + " != null");

            result.Name = $"{subTexture.m_Name}@{highlightIndex.ToString()}";
            result.NameEnglish = result.Name;

            result.Ambient = material.Ambient;
            result.Diffuse = material.Diffuse;
            result.Specular = material.Specular;
            result.EdgeColor = material.EdgeColor;
            result.EdgeSize = material.EdgeSize;

            result.SphereTextureFileName = bakedMaterial.TextureName;
            result.SphereTextureMode = SphereTextureMode.SubTexture;

            result.Flags = material.Flags;
            result.ToonTextureFileName = material.ToonTextureFileName;

            return result;
        }

        [NotNull]
        private static BakedMaterial CreateBakedMaterialFromBaseTexture([NotNull] string textureName, [NotNull] TexturedMaterial material) {
            var mainTexture = material.MainTexture;
            Bitmap baseImage;

            if (material.HasMainTexture) {
                if (mainTexture != null) {
                    baseImage = mainTexture.ConvertToBitmap(material.ExtraProperties.ShouldFlip);
                } else {
                    baseImage = Create1x1WhiteBitmap();
                }
            } else {
                throw new InvalidOperationException($"Cannot bake: desired main texture \"{textureName}\" does not exist.");
            }

            return new BakedMaterial(textureName, baseImage);
        }

        [NotNull]
        private static BakedMaterial CreateHighlightBakedMaterial([NotNull] string textureName, [NotNull] TexturedMaterial material) {
            var subTexture = material.SubTexture;
            Bitmap baseImage;

            if (material.HasSubTexture) {
                if (subTexture != null) {
                    baseImage = subTexture.ConvertToBitmap(material.ExtraProperties.ShouldFlip);
                } else {
                    baseImage = Create1x1WhiteBitmap();
                }
            } else {
                throw new InvalidOperationException($"Cannot bake: desired sub texture \"{textureName}\" does not exist.");
            }

            return new BakedMaterial(textureName, baseImage);
        }

        [NotNull]
        // ReSharper disable once InconsistentNaming
        private static Bitmap Create1x1WhiteBitmap() {
            var bitmap = new Bitmap(1, 1, PixelFormat.Format32bppArgb);
            bitmap.SetPixel(0, 0, Color.White);
            return bitmap;
        }

        [NotNull, ItemNotNull]
        private PmxMorph[] AddEmotionMorphs([NotNull] PrettyMesh mesh) {
            var s = mesh.Shape;

            if (s == null) {
                return Array.Empty<PmxMorph>();
            }

            Debug.Assert(s.Channels.Length == s.Shapes.Length, "s.Channels.Count == s.Shapes.Count");
            Debug.Assert(s.Channels.Length == s.FullWeights.Length, "s.Channels.Count == s.FullWeights.Count");

            var morphCount = s.Channels.Length;
            var morphs = new List<PmxMorph>();

            for (var i = 0; i < morphCount; i++) {
                var channel = s.Channels[i];
                var shape = s.Shapes[i];
                var vertices = s.Vertices;
                var morph = new PmxMorph();

                // Some models has the name "blendShape1.[morph]" (ch_gs001_201xxx), and some "[long serial prefix]_blendShape[n].[morph]" (ch_ss001_017kth)
                var morphNameDotIndex = channel.Name.LastIndexOf('.');
                string morphName;

                if (morphNameDotIndex < 0) {
                    morphName = channel.Name;
                } else {
                    morphName = channel.Name.Substring(morphNameDotIndex + 1);
                }

                if (_conversionConfig.TranslateFacialExpressionNamesToMmd) {
                    morph.Name = MorphUtils.LookupMorphName(morphName);
                } else {
                    morph.Name = morphName;
                }

                morph.NameEnglish = morphName;

                morph.OffsetKind = MorphOffsetKind.Vertex;

                var offsets = new List<PmxBaseMorph>();

                for (var j = shape.FirstVertex; j < shape.FirstVertex + shape.VertexCount; ++j) {
                    var v = vertices[(int)j];
                    var m = new PmxVertexMorph();

                    var offset = v.Vertex.ToOpenTK().FixUnityToMmd();

                    if (_conversionConfig.ScaleToPmxSize) {
                        offset = offset * _scalingConfig.ScaleUnityToPmx;
                    }

                    m.Index = (int)v.Index;
                    m.Offset = offset;

                    offsets.Add(m);
                }

                morph.Offsets = offsets.ToArray();

                morphs.Add(morph);
            }

            // Now some custom morphs for our model to be compatible with TDA.
            morphs.Add(CreateCompositeMorph(s, "E_metoji", "E_metoji_l", "E_metoji_r"));

            return morphs.ToArray();
        }

        [NotNull, ItemNotNull]
        private static PmxNode[] AddBoneNodesAsGroups([NotNull, ItemNotNull] PmxBone[] bones, [NotNull, ItemNotNull] PmxMorph[] morphs) {
            var nodes = new List<PmxNode>();

            nodes.Add(CreateBoneGroup(bones, "Root", "Root", "操作中心"));
            nodes.Add(CreateEmotionNode(morphs));
            nodes.Add(CreateBoneGroup(bones, "センター", "Center", "全ての親", "センター"));
            nodes.Add(CreateBoneGroup(bones, "ＩＫ", "IK", "左足IK親", "左足ＩＫ", "左つま先ＩＫ", "右足IK親", "右足ＩＫ", "右つま先ＩＫ"));
            nodes.Add(CreateBoneGroup(bones, "体(上)", "Upper Body", "上半身", "上半身2", "首", "頭"));
            nodes.Add(CreateBoneGroup(bones, "腕", "Arms", "左肩", "左腕", "左ひじ", "左手首", "右肩", "右腕", "右ひじ", "右手首"));
            nodes.Add(CreateBoneGroup(bones, "手", "Hands", "左親指１", "左親指２", "左親指３", "左人指１", "左人指２", "左人指３", "左ダミー", "左中指１", "左中指２", "左中指３", "左薬指１", "左薬指２", "左薬指３", "左小指１", "左小指２", "左小指３",
                "右親指１", "右親指２", "右親指３", "右人指１", "右人指２", "右人指３", "右ダミー", "右中指１", "右中指２", "右中指３", "右薬指１", "右薬指２", "右薬指３", "右小指１", "右小指２", "右小指３"));
            nodes.Add(CreateBoneGroup(bones, "体(下)", "Lower Body", "グルーブ", "腰", "下半身"));
            nodes.Add(CreateBoneGroup(bones, "足", "Legs", "左足", "左ひざ", "左足首", "左つま先", "右足", "右ひざ", "右足首", "右つま先"));
            nodes.Add(CreateBoneGroup(bones, "その他", "Others", "両目", "左目", "右目"));

            return nodes.ToArray();
        }

        [NotNull]
        private static PmxNode CreateBoneGroup([NotNull, ItemNotNull] PmxBone[] bones, [NotNull] string groupNameJp, [NotNull] string groupNameEn, [NotNull, ItemNotNull] params string[] boneNames) {
            var node = new PmxNode();

            node.Name = groupNameJp;
            node.NameEnglish = groupNameEn;

            var boneNodes = new List<NodeElement>();

            foreach (var boneName in boneNames) {
                var bone = bones.Find(b => b.Name == boneName);

                if (bone != null) {
                    boneNodes.Add(new NodeElement {
                        ElementType = ElementType.Bone,
                        Index = bone.BoneIndex
                    });
                } else {
                    Debug.WriteLine($"Warning: bone node not found: {boneName}");
                }
            }

            node.Elements = boneNodes.ToArray();

            return node;
        }

        [NotNull]
        private static PmxNode CreateEmotionNode([NotNull, ItemNotNull] PmxMorph[] morphs) {
            var node = new PmxNode();

            node.Name = "表情";
            node.NameEnglish = "Facial Expressions";

            var elements = new List<NodeElement>();
            var morphCount = morphs.Length;

            for (var i = 0; i < morphCount; ++i) {
                var elem = new NodeElement();

                elem.ElementType = ElementType.Morph;
                elem.Index = i;

                elements.Add(elem);
            }

            node.Elements = elements.ToArray();

            return node;
        }

        [NotNull]
        private PmxMorph CreateCompositeMorph([NotNull] BlendShapeData blendShape, [NotNull] string mltdTruncMorphName, [NotNull, ItemNotNull] params string[] truncNames) {
            var morph = new PmxMorph();

            if (_conversionConfig.TranslateFacialExpressionNamesToMmd) {
                morph.Name = MorphUtils.LookupMorphName(mltdTruncMorphName);
            } else {
                morph.Name = mltdTruncMorphName;
            }

            morph.NameEnglish = mltdTruncMorphName;

            var offsets = new List<PmxBaseMorph>();
            var vertices = blendShape.Vertices;

            var matchedChannels = truncNames.Select(name => {
                // name: e.g. "E_metoji_l"
                // ch_ex005_016tsu has "blendShape2.E_metoji_l" instead of the common one "blendShape1.E_metoji_l"
                // so the old method (string equal to full name) breaks.
                var chan = blendShape.Channels.SingleOrDefault(ch => ch.Name.EndsWith(name));

                if (chan == null) {
                    Trace.WriteLine($"Warning: required blend channel not found: {name}");
                }

                return chan;
            }).ToArray();

            foreach (var channel in matchedChannels) {
                if (channel == null) {
                    continue;
                }

                var channelIndex = blendShape.Channels.IndexOf(channel);
                var shape = blendShape.Shapes[channelIndex];

                morph.OffsetKind = MorphOffsetKind.Vertex;

                for (var j = shape.FirstVertex; j < shape.FirstVertex + shape.VertexCount; ++j) {
                    var v = vertices[(int)j];
                    var m = new PmxVertexMorph();

                    var offset = v.Vertex.ToOpenTK().FixUnityToMmd();

                    if (_conversionConfig.ScaleToPmxSize) {
                        offset = offset * _scalingConfig.ScaleUnityToPmx;
                    }

                    m.Index = (int)v.Index;
                    m.Offset = offset;

                    offsets.Add(m);
                }
            }

            morph.Offsets = offsets.ToArray();

            return morph;
        }

        [NotNull, ItemNotNull]
        // ReSharper disable once InconsistentNaming
        private static PmxBone[] CreateLegIK([NotNull, ItemNotNull] List<PmxBone> bones, [NotNull] string leftRightJp, [NotNull] string leftRightEn) {
            var startBoneCount = bones.Count;

            PmxBone ikParent = new PmxBone(), ikBone = new PmxBone();

            ikParent.Name = $"{leftRightJp}足IK親";
            ikParent.NameEnglish = $"leg IKP_{leftRightEn}";
            ikBone.Name = $"{leftRightJp}足ＩＫ";
            ikBone.NameEnglish = $"leg IK_{leftRightEn}";

            PmxBone master;

            do {
                master = bones.Find(b => b.Name == "全ての親");

                if (master == null) {
                    throw new ArgumentException("Missing master bone.");
                }
            } while (false);

            ikParent.ParentIndex = bones.IndexOf(master);
            ikBone.ParentIndex = startBoneCount; // IKP
            ikParent.SetFlag(BoneFlags.ToBone);
            ikBone.SetFlag(BoneFlags.ToBone);
            ikParent.To_Bone = startBoneCount + 1; // IK
            ikBone.To_Bone = -1;

            PmxBone ankle, knee, leg;

            do {
                var ankleName = $"{leftRightJp}足首";
                ankle = bones.Find(b => b.Name == ankleName);
                var kneeName = $"{leftRightJp}ひざ";
                knee = bones.Find(b => b.Name == kneeName);
                var legName = $"{leftRightJp}足";
                leg = bones.Find(b => b.Name == legName);

                if (ankle == null) {
                    throw new ArgumentException("Missing ankle bone.");
                }

                if (knee == null) {
                    throw new ArgumentException("Missing knee bone.");
                }

                if (leg == null) {
                    throw new ArgumentException("Missing leg bone.");
                }
            } while (false);

            ikBone.CurrentPosition = ikBone.InitialPosition = ankle.InitialPosition;
            ikParent.CurrentPosition = ikParent.InitialPosition = new Vector3(ikBone.InitialPosition.X, 0, ikBone.InitialPosition.Z);

            ikParent.SetFlag(BoneFlags.Translation | BoneFlags.Rotation);
            ikBone.SetFlag(BoneFlags.Translation | BoneFlags.Rotation | BoneFlags.IK);

            var ik = new PmxIK();

            ik.LoopCount = 10;
            ik.AngleLimit = MathHelper.DegreesToRadians(114.5916f);
            ik.TargetBoneIndex = bones.IndexOf(ankle);

            var links = new IKLink[2];

            links[0] = new IKLink();
            links[0].BoneIndex = bones.IndexOf(knee);
            links[0].IsLimited = true;
            links[0].LowerBound = new Vector3(MathHelper.DegreesToRadians(-180), 0, 0);
            links[0].UpperBound = new Vector3(MathHelper.DegreesToRadians(-0.5f), 0, 0);
            links[1] = new IKLink();
            links[1].BoneIndex = bones.IndexOf(leg);

            ik.Links = links;
            ikBone.IK = ik;

            return new[] {
                ikParent, ikBone
            };
        }

        [NotNull, ItemNotNull]
        private static PmxBone[] CreateToeIK([NotNull, ItemNotNull] List<PmxBone> bones, [NotNull] string leftRightJp, [NotNull] string leftRightEn) {
            PmxBone ikParent, ikBone = new PmxBone();

            do {
                var parentName = $"{leftRightJp}足ＩＫ";

                ikParent = bones.Find(b => b.Name == parentName);

                Debug.Assert(ikParent != null, nameof(ikParent) + " != null");
            } while (false);

            ikBone.Name = $"{leftRightJp}つま先ＩＫ";
            ikBone.NameEnglish = $"toe IK_{leftRightEn}";

            ikBone.ParentIndex = bones.IndexOf(ikParent);

            ikBone.SetFlag(BoneFlags.ToBone);
            ikBone.To_Bone = -1;

            PmxBone toe, ankle;

            do {
                var toeName = $"{leftRightJp}つま先";
                toe = bones.Find(b => b.Name == toeName);
                var ankleName = $"{leftRightJp}足首";
                ankle = bones.Find(b => b.Name == ankleName);

                if (toe == null) {
                    throw new ArgumentException("Missing toe bone.");
                }

                if (ankle == null) {
                    throw new ArgumentException("Missing ankle bone.");
                }
            } while (false);

            ikBone.CurrentPosition = ikBone.InitialPosition = toe.InitialPosition;
            ikBone.SetFlag(BoneFlags.Translation | BoneFlags.Rotation | BoneFlags.IK);

            var ik = new PmxIK();

            ik.LoopCount = 10;
            ik.AngleLimit = MathHelper.DegreesToRadians(114.5916f);
            ik.TargetBoneIndex = bones.IndexOf(toe);

            var links = new IKLink[1];

            links[0] = new IKLink();
            links[0].BoneIndex = bones.IndexOf(ankle);

            ik.Links = links.ToArray();
            ikBone.IK = ik;

            return new[] {
                ikBone
            };
        }

        private static (int VertexStart1, int VertexCount1, int VertexStart2, int VertexCount2) FindEyesVerticesRange([NotNull] CompositeMesh combinedMesh) {
            var meshNameIndex = -1;

            Debug.Assert(combinedMesh != null, nameof(combinedMesh) + " != null");

            for (var i = 0; i < combinedMesh.Names.Length; i++) {
                var meshName = combinedMesh.Names[i];

                if (meshName == "eyes") {
                    meshNameIndex = i;
                    break;
                }
            }

            if (meshNameIndex < 0) {
                throw new ArgumentException("Mesh \"eyes\" is missing.");
            }

            var subMeshMaps = combinedMesh.ParentMeshIndices.EnumerateAll().WhereToArray(s => s.Value == meshNameIndex);

            Debug.Assert(subMeshMaps.Length == 2, "There should be 2 sub mesh maps.");
            Debug.Assert(subMeshMaps[1].Index - subMeshMaps[0].Index == 1, "The first sub mesh map should contain one element.");

            var vertexStart1 = (int)combinedMesh.SubMeshes[subMeshMaps[0].Index].FirstVertex;
            var vertexCount1 = (int)combinedMesh.SubMeshes[subMeshMaps[0].Index].VertexCount;
            var vertexStart2 = (int)combinedMesh.SubMeshes[subMeshMaps[1].Index].FirstVertex;
            var vertexCount2 = (int)combinedMesh.SubMeshes[subMeshMaps[1].Index].VertexCount;

            return (vertexStart1, vertexCount1, vertexStart2, vertexCount2);
        }

        private static Vector3 GetEyeBonePosition([NotNull, ItemNotNull] PmxVertex[] vertices, int vertexStart, int vertexCount) {
            var centerPos = Vector3.Zero;
            var leftMostPos = new Vector3(float.MinValue, 0, 0);
            var rightMostPos = new Vector3(float.MaxValue, 0, 0);
            int leftMostIndex = -1, rightMostIndex = -1;

            for (var i = vertexStart; i < vertexStart + vertexCount; ++i) {
                var pos = vertices[i].Position;

                centerPos += pos;

                if (pos.X > leftMostPos.X) {
                    leftMostPos = pos;
                    leftMostIndex = i;
                }

                if (pos.X < rightMostPos.X) {
                    rightMostPos = pos;
                    rightMostIndex = i;
                }
            }

            Debug.Assert(leftMostIndex >= 0, nameof(leftMostIndex) + " >= 0");
            Debug.Assert(rightMostIndex >= 0, nameof(rightMostIndex) + " >= 0");

            centerPos = centerPos / vertexCount;

            // "Eyeball". You got the idea?
            var leftMostNorm = vertices[leftMostIndex].Normal;
            var rightMostNorm = vertices[rightMostIndex].Normal;

            var k1 = leftMostNorm.Z / leftMostNorm.X;
            var k2 = rightMostNorm.Z / rightMostNorm.X;
            float x1 = leftMostPos.X, x2 = rightMostPos.X, z1 = leftMostPos.Z, z2 = rightMostPos.Z;

            var d1 = (z2 - k2 * x2 + k2 * x1 - z1) / (k1 - k2);

            var x = x1 + d1;
            var z = z1 + k1 * d1;

            return new Vector3(x, centerPos.Y, z);
        }

        private static Vector3 GetEyesBonePosition([NotNull, ItemNotNull] PmxVertex[] vertices, int vertexStart1, int vertexCount1, int vertexStart2, int vertexCount2) {
            var result = new Vector3();

            for (var i = vertexStart1; i < vertexStart1 + vertexCount1; ++i) {
                result += vertices[i].Position;
            }

            for (var i = vertexStart2; i < vertexStart2 + vertexCount2; ++i) {
                result += vertices[i].Position;
            }

            result = result / (vertexCount1 + vertexCount2);

            return new Vector3(0, result.Y + 0.5f, -0.6f);
        }

        [CanBeNull]
        private static BoneNode GetDirectSingleChildOf([NotNull] BoneNode b) {
            var foundAndSingle = false;
            BoneNode result = null;

            foreach (var c in b.Children) {
                var isGenerated = BoneLookup.IsNameGenerated(c.Path);

                if (isGenerated) {
                    continue;
                }

                if (result == null) {
                    result = c;
                    foundAndSingle = true;
                } else {
                    foundAndSingle = false;
                }
            }

            return foundAndSingle ? result : null;
        }

        [NotNull]
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static string GetExportedTextureFileName([NotNull] string texturePrefix, int textureIndex) {
            var textureIndexStr = textureIndex.ToString("00");
            return $"{texturePrefix}{textureIndexStr}.png";
        }

        private static MaterialKind CategorizeMaterial([NotNull] string materialName) {
            if (materialName.Contains("skin")) {
                return MaterialKind.BodySkin;
            } else if (materialName.Contains("face")) {
                return MaterialKind.FacialSkin;
            } else if (materialName.Contains("hair")) {
                return MaterialKind.Hair;
            } else if (materialName.Contains("eye")) {
                return MaterialKind.Eyes;
            } else if (materialName.Contains("cut") || materialName.Contains("rct") || materialName.Contains("acc") || materialName.Contains("chr")) {
                return MaterialKind.Accessories;
            } else {
                return MaterialKind.Clothes;
            }
        }

        private enum MaterialKind {

            BodySkin = 0,

            FacialSkin = 1,

            Hair = 2,

            Eyes = 3,

            Clothes = 4,

            Accessories = 5,

        }

    }
}
